# Union.java

    package org.codefilarete.stalactite.query.model;

    import javax.annotation.Nullable;
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Map.Entry;
    import java.util.Objects;
    import java.util.Set;

    import org.codefilarete.stalactite.query.api.*;
    import org.codefilarete.stalactite.sql.ddl.structure.Column;
    import org.codefilarete.tool.Reflections;
    import org.codefilarete.tool.collection.KeepOrderSet;

    /**
     * Defines a SQL Union between several queries.
     * Available {@link Column}s as selectable elements must be declared through {@link #registerColumn(String, Class)}
     *
     * @author Guillaume Mary
     */
    public class Union implements QueryStatement, UnionAware, QueryProvider<Union> {

    	private final KeepOrderSet<Query> queries;

    	private final Map<Selectable<?>, String> aliases = new HashMap<>();

    	private final KeepOrderSet<PseudoColumn<Object>> columns = new KeepOrderSet<>();

    	public Union(Collection<Query> queries) {
    		this.queries = new KeepOrderSet<>(queries);
    	}

    	public Union(Query ... queries) {
    		this.queries = new KeepOrderSet<>(queries);
    	}

    	public KeepOrderSet<Query> getQueries() {
    		return queries;
    	}

    	@Override
    	public Union getQuery() {
    		return this;
    	}

    	/**
    	 * Declares a column to this union.
    	 * May do nothing if a column already exists with same name and type.
    	 * Will throw an exception if a column with same name but with different type already exists.
    	 *
    	 * @param expression column name
    	 * @param javaType column type
    	 * @param <O> column type
    	 * @return the created column or the existing one
    	 */
    	public <O> PseudoColumn<O> addColumn(String expression, Class<O> javaType) {
    		return addColumn(expression, javaType, null);
    	}

    	/**
    	 * Declares a column to this union with an alias.
    	 * May do nothing if a column already exists with same name and type.
    	 * Will throw an exception if a column with same name but with different type already exists.
    	 *
    	 * @param expression column name
    	 * @param javaType column type
    	 * @param alias column alias (optional)
    	 * @param <O> column type
    	 * @return the created column or the existing one
    	 */
    	public <O> PseudoColumn<O> addColumn(String expression, Class<O> javaType, @Nullable String alias) {
    		return addertColumn(new PseudoColumn<>(this, expression, javaType), alias);
    	}

    	/**
    	 * Adds a column with presence assertion (add + assert = addert, poor naming)
    	 *
    	 * @param <O> column type
    	 * @param column the column to be added
    	 * @param alias column alias (optional)
    	 * @return given column
    	 */
    	private <O> PseudoColumn<O> addertColumn(PseudoColumn<O> column, @Nullable String alias) {
    		// Quite close to Table.addertColumn(..)
    		PseudoColumn<O> existingColumn = findColumn(column.getExpression(), alias);
    		if (existingColumn != null && (Objects.equals(alias, aliases.get(existingColumn))) && !existingColumn.getJavaType().equals(column.getJavaType())) {
    			throw new IllegalArgumentException("Trying to add a column '" + existingColumn.getExpression() + "' that already exists with a different type : "
    													   + Reflections.toString(existingColumn.getJavaType()) + " vs " + Reflections.toString(column.getJavaType()));
    		}
    		if (existingColumn == null) {
    			columns.add((PseudoColumn<Object>) column);
    			return column;
    		} else {
    			return existingColumn;
    		}
    	}

    	private <C extends Selectable<?>> C findColumn(String columnName, @Nullable String alias) {
    		if (alias != null) {
    			for (Entry<? extends Selectable<?>, String> aliasPawn : getAliases().entrySet()) {
    				if (aliasPawn.getValue().equals(alias)) {
    					return (C) aliasPawn.getKey();
    				}
    			}
    		} else {
    			for (Selectable<?> column : getColumns()) {
    				if (column instanceof JoinLink && column.getExpression().equals(columnName)) {
    					return (C) column;
    				}
    			}
    		}
    		return null;
    	}

    	public <O> PseudoColumn<O> registerColumn(String expression, Class<O> javaType) {
    		return addColumn(expression, javaType);
    	}

    	public <O> PseudoColumn<O> registerColumn(String expression, Class<O> javaType, String alias) {
    		return addColumn(expression, javaType, alias);
    	}

    	@Override
    	public Set<PseudoColumn<?>> getColumns() {
    		return (Set) columns;
    	}

    	@Override
    	public Map<Selectable<?>, String> getAliases() {
    		return aliases;
    	}

    	@Override
    	public Union unionAll(QueryProvider<Query> query) {
    		queries.add(query.getQuery());
    		return this;
    	}

    	/**
    	 * @return columns of this instance per their name and alias
    	 */
    	@Override
    	public Map<String, Selectable<?>> mapColumnsOnName() {
    		Map<String, Selectable<?>> result = new HashMap<>();
    		for (PseudoColumn<?> column : getColumns()) {
    			result.put(column.getExpression(), column);
    		}
    		for (Entry<? extends Selectable<?>, String> alias : getAliases().entrySet()) {
    			result.put(alias.getValue(), alias.getKey());
    		}
    		return result;
    	}
    }